namespace ScrumSprintMonitor.Wpf.Application
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.ComponentModel.Composition;
	using System.ComponentModel.Composition.Hosting;
	using System.Diagnostics;
	using System.IO;
	using System.Linq;
	using System.Reflection;
	using System.Security.Permissions;
	using System.Windows;
	using System.Windows.Forms;
	using System.Windows.Interop;
	using System.Windows.Threading;
	using System.Xml;

	using Extensibility;
	using Extensibility.Events;

	using Implementation;

	using Infrastructure;
	using Infrastructure.Configuration;
	using Infrastructure.IoC;

	using log4net.Config;

	using Microsoft.Practices.Composite.Events;
	using Microsoft.Practices.Composite.Logging;
	using Microsoft.Practices.Composite.Modularity;
	using Microsoft.Practices.Composite.Presentation.Regions;
	using Microsoft.Practices.Composite.Regions;
	using Microsoft.Practices.Composite.UnityExtensions;
	using Microsoft.Practices.ServiceLocation;
	using Microsoft.Practices.Unity;
	using Microsoft.SDK.Samples.VistaBridge.Services;

	using PreEmptive.Attributes;
	
	using Properties;

	using UI.Wpf;
	using UI.Wpf.Avatars;

	using Application = System.Windows.Application;
	using DesignerProperties = Infrastructure.DesignerProperties;
	using MessageBox = System.Windows.MessageBox;

	#endregion

	public class Bootstrapper : UnityBootstrapper,
								IDisposable
	{
		private readonly IList<ScrumSprintMonitorAddIn> addInsList = new List<ScrumSprintMonitorAddIn>();
		private readonly IList<Infrastructure.IoC.IModule> additionalContainerModulesList = new List<Infrastructure.IoC.IModule>();
		private readonly Assembly assembly = Assembly.GetExecutingAssembly();
		private CompositionContainer container;
		private IEventAggregator eventAggregator;
		private ILogger<App> logger;
		private ILoggerFacade loggerFacade;
		private UnityContainerAbstraction unityContainerAbstraction;
		private Window primaryMonitorShell;
		private IShellLayoutConfiguration shellLayoutConfiguration;
		private PowerManager powerManager;
		private bool isShuttingDown;

		[SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
		public Bootstrapper()
		{
			if (this.assembly != null)
			{
				Debug = this.assembly.Location.EndsWith("EXE", StringComparison.OrdinalIgnoreCase);
			}

			DesignerProperties.GetIsInDesignMode = (object x) => StubDesignerProperties.GetIsInDesignMode(null);

			Application.Current.DispatcherUnhandledException += OnApplicationDispatcherUnhandledException;
			Application.Current.Exit += OnApplicationExit;

			Configlog4net();
		}

		[ImportMany(typeof(ScrumSprintMonitorAddIn))]
		public IList<ScrumSprintMonitorAddIn> AddIns
		{
			[DebuggerStepThrough]
			get { return this.addInsList; }
		}

		[ImportMany(typeof(Infrastructure.IoC.IModule))]
		public IList<Infrastructure.IoC.IModule> AdditionalContainerModules
		{
			[DebuggerStepThrough]
			get { return this.additionalContainerModulesList; }
		}

		internal static bool Debug { get; set; }

		protected override ILoggerFacade LoggerFacade
		{
			[DebuggerStepThrough]
			get { return this.loggerFacade ?? (this.loggerFacade = new Log4NetLoggerFacade()); }
		}

		#region IDisposable Members

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion

		~Bootstrapper()
		{
			Dispose(false);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (disposing)
			{
				if (this.powerManager != null)
				{
					this.powerManager.Dispose();
					this.powerManager = null;
				}
				if (this.container != null)
				{
					this.container.Dispose();
					this.container = null;
				}
			}
		}

		#region Implementation

		[SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
		private void Configlog4net()
		{
#if DEBUG
			const string logConfigFileName = "log4net.debug.config.xml";
			string initialLogConfig = Resources.log4net_debug_config;
#else
			const string logConfigFileName = "log4net.release.config.xml";
			string initialLogConfig = Resources.log4net_release_config;
#endif
			const string logFileBaseName = "scrumsprintmonitor.log";
			string assemblyLocation = this.assembly.Location;
			var versionInfo = FileVersionInfo.GetVersionInfo(assemblyLocation);
			string appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
												versionInfo.CompanyName.Replace(' ', '_'));
			string configFilePath = Path.Combine(appDataFolder, logConfigFileName);

			//when the config file changes and you need to implement a new one, 
			//   uncomment this so it deletes the current one so it will copy over a new one 
			//if (File.Exists(configFilePath))
			//{
			//    try
			//    {
			//        File.SetAttributes(configFilePath, FileAttributes.Normal);
			//        File.Delete(configFilePath);
			//    }
			//    catch
			//    {
			//        //don't do anything, just let it go
			//    }
			//}

			var configFile = new FileInfo(configFilePath);

			if (!configFile.Exists)
			{
				Directory.CreateDirectory(Path.GetDirectoryName(configFilePath));

				//if they don't already have a config file, go find it and copy it 
				using (var writer = File.CreateText(configFile.FullName))
				{
					var document = new XmlDocument();
					document.LoadXml(initialLogConfig);

					foreach (XmlNode node in document.GetElementsByTagName("file"))
					{
						node.Attributes.GetNamedItem("value").Value = Path.Combine(appDataFolder, logFileBaseName);
						break;
					}

					document.Save(writer);
				}
			}
			else
			{
				var document = new XmlDocument();
				document.Load(configFilePath);

				foreach (XmlNode node in document.GetElementsByTagName("file"))
				{
					string existingLogFilePath = node.Attributes.GetNamedItem("value").Value;
					string logFilePath = Path.Combine(appDataFolder, logFileBaseName);
					if (logFilePath != existingLogFilePath)
					{
						node.Attributes.GetNamedItem("value").Value = logFilePath;

						document.Save(configFilePath);
					}
					break;
				}
			}

			XmlConfigurator.Configure(configFile);
		}

		private bool Compose()
		{
			this.logger.Debug("Composing MEF container...");

			var aggregateCatalog = new AggregateCatalog();
			string binFolder = Path.GetDirectoryName(this.assembly.Location);
			string addInsBinFolder = Path.Combine(binFolder, @"AddIns");
			string serverAdaptersBinFolder = Path.Combine(binFolder, @"AddIns\ServerAdapters");

			aggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(WpfUriAvatar).Assembly));
			try
			{
				AddCatalogFromFolderPath(aggregateCatalog, serverAdaptersBinFolder);
			}
			catch (ReflectionTypeLoadException e)
			{
				foreach (var loaderEx in e.LoaderExceptions)
				{
					this.logger.Fatal(loaderEx, "Loader exception!");
				}
				
				if (e.LoaderExceptions.OfType<FileNotFoundException>().Any(x => x.Message.Contains("TeamFoundation")))
				{
					MessageBox.Show("Could not find Team Explorer 2010 installed. Please make sure the prerequisites are installed before retrying.", null, MessageBoxButton.OK, MessageBoxImage.Stop);

					return false;	
				}

				throw;
			}
			AddCatalogFromFolderPath(aggregateCatalog, addInsBinFolder);

			this.logger.Debug("Found {0} parts in {1} MEF catalogs:", aggregateCatalog.Parts.Count(), aggregateCatalog.Catalogs.Count);
			System.Diagnostics.Debug.Assert(aggregateCatalog.Parts.Any());

			this.container = new CompositionContainer(aggregateCatalog);

			var batch = new CompositionBatch();
			batch.AddPart(this);
			batch.AddPart(Container.Resolve<IProcessTemplateAdapterProvider>());
			batch.AddPart(Container.Resolve<IAvatarProviderStore>());
			batch.AddExportedValue(Container.Resolve<IServiceLocator>());
			batch.AddExportedValue<ICompositionService>(this.container);

			try
			{
				this.container.Compose(batch);
			}
			catch (CompositionException compositionException)
			{
				this.logger.Fatal(compositionException, "Found exception while composing MEF container.");

				MessageBox.Show(compositionException.ToString());
				return false;
			}

			return true;
		}

		private void AddCatalogFromFolderPath(AggregateCatalog aggregateCatalog, string binFolder)
		{
			if (Directory.Exists(binFolder))
			{
				try
				{
					this.logger.Debug("Creating MEF DirectoryCatalog from {0}...", binFolder);

					var addinsDirectoryCatalog = new DirectoryCatalog(binFolder);
					aggregateCatalog.Catalogs.Add(addinsDirectoryCatalog);

					foreach (var part in addinsDirectoryCatalog.Parts)
					{
						this.logger.Debug("{0}", part.ToString());
					}
				}
				catch (Exception e)
				{
					this.logger.Fatal(e, "Found exception while addint catalog to MEF.");

					throw;
				}
			}
			else
			{
				this.logger.Debug("{0} addin folder not found.", binFolder);
			}
		}

		[SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
		private void ProcessCommandLine()
		{
			try
			{
				var args = Environment.GetCommandLineArgs();
				if (args.Length > 1 && !string.IsNullOrEmpty(Settings.Default.TfsServerName))
				{
					// Get the 2 character command line argument
					string arg = args[1].ToUpperInvariant().Trim().Substring(0, 2);
					switch (arg)
					{
						case "/C":
							// Show the options dialog
							ConfigureScreensaver();
							ShutdownApplication();
							break;
						case "/P":
							// Don't do anything for preview
							ShutdownApplication();
							break;
						case "/S":
							// Show screensaver form
							RunAsScreenSaver();
							break;
						default:
							ShutdownApplication();
							break;
					}
				}
				else
				{
					if (Debug)
					{
						RunAsDesktopApplication();
					}
					else
					{
						// If no arguments were passed in, configure the screensaver
						ConfigureScreensaver();
						ShutdownApplication();
					}
				}
			}
			catch (IOException ioException)
			{
				this.logger.Error(ioException, "Exception found while starting app.");

				MessageBox.Show(ioException.Message);

				ShutdownApplication();

				this.primaryMonitorShell = null;
			}
		}

		[Feature("Run as Screen Saver")]
		private void RunAsScreenSaver()
		{
			ShowShell();
		}

		[Feature("Run as Desktop Application")]
		private void RunAsDesktopApplication()
		{
			Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;

			if (System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed)
			{
				OnClickOnceExecution();
			}

			// If no arguments were passed in, configure the screensaver
			if (ConfigureScreensaver())
			{
				Settings.Default.Refresh();

				ShowShell();
			}
			else
			{
				ShutdownApplication();
			}
		}

		[Feature("Run as ClickOnce Application")]
		private static void OnClickOnceExecution()
		{
			// No-op
		}

		private void ShutdownApplication()
		{
			if (!this.isShuttingDown)
			{
				this.isShuttingDown = true;
				Application.Current.Shutdown();
			}
		}

		[SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
		private bool ConfigureScreensaver()
		{
			bool isTopMost = false;

			if (this.primaryMonitorShell != null)
			{
				isTopMost = this.primaryMonitorShell.Topmost;

				this.primaryMonitorShell.Topmost = false;
			}

			string binFolderPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
			string configurationEditorExe = Path.Combine(binFolderPath, "ScrumSprintMonitor.ConfigEditor.exe");

			try
			{
				int exitCode = UACManager.ExecutePrivilegedProcess(configurationEditorExe);
				return (exitCode == 1);
			}
			catch (Win32Exception ex)
			{
				System.Diagnostics.Debug.Assert(ex.NativeErrorCode == 1223); // E_ABORT
				return false;
			}
			finally
			{
				if (isTopMost)
				{
					this.primaryMonitorShell.Topmost = true;
				}
			}
		}

		/// <summary>
		///   Shows screen saver by creating one instance of MainWindow for each monitor.
		/// 
		///   Note: uses WinForms's Screen class to get monitor info.
		/// </summary>
		private Window ShowShell()
		{
			UnityContainerAbstraction.EnsureConfigurationDependentModulesAreSetUp();

			this.shellLayoutConfiguration = Container.Resolve<IShellLayoutConfiguration>();
			this.powerManager = Container.Resolve<PowerManager>();

			this.logger.Info("Starting screensaver...");

			this.eventAggregator.GetEvent<InvokeConfigurationUiEvent>().Subscribe(OnInvokeConfigurationUI);

			bool multiMonitor = this.shellLayoutConfiguration.MultiMonitor;

			//creates window on primary screen
			var primaryShell = Container.Resolve<PrimaryMonitorShell>();
			primaryShell.WindowStartupLocation = WindowStartupLocation.Manual;
			if (Debug)
			{
				primaryShell.WindowStyle = WindowStyle.ToolWindow;
				primaryShell.ShowInTaskbar = true;
			}
			primaryShell.SourceInitialized += delegate
											  {
												var source = (HwndSource) PresentationSource.FromVisual(primaryShell);
												if (source != null)
												{
													source.AddHook(MessageHook);
												}
											  };

			this.primaryMonitorShell = primaryShell;
			MakeWindowFillThisMonitor(Screen.AllScreens.First(), primaryShell);

			ShowMonitorWindow(primaryShell);

			if (multiMonitor)
			{
				var secondaryMonitor = Screen.AllScreens[1 % Screen.AllScreens.Length];

				var secondaryShell = Container.Resolve<SecondaryMonitorShell>();
				secondaryShell.WindowStartupLocation = WindowStartupLocation.Manual;
				if (Debug)
				{
					secondaryShell.WindowStyle = WindowStyle.ToolWindow;
					secondaryShell.ShowInTaskbar = true;
				}
				MakeWindowFillThisMonitor(secondaryMonitor, secondaryShell);

				//show non-primary screen windows
				ShowMonitorWindow(secondaryShell);

				RegionManager.SetRegionManager(secondaryShell, Container.Resolve<IRegionManager>());
				RegionManager.UpdateRegions();
			}

			Application.Current.MainWindow = primaryShell;
			Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

			this.logger.Info(multiMonitor ? "Started screensaver in multi-monitor mode." : "Started screensaver in single monitor mode.", Category.Info,
							 Priority.Low);

			this.powerManager.Start();

			return primaryShell;
		}

		private static void MakeWindowFillThisMonitor(Screen monitor, Window shell)
		{
			var location = monitor.Bounds;
			if (shell.WindowStyle == WindowStyle.ToolWindow)
			{
				//covers entire monitor
				shell.Left = location.X - SystemParameters.ResizeFrameVerticalBorderWidth;
				shell.Top = location.Y - SystemParameters.ResizeFrameHorizontalBorderHeight;
				shell.Width = location.Width + SystemParameters.ResizeFrameVerticalBorderWidth * 2;
				shell.Height = location.Height + SystemParameters.ResizeFrameHorizontalBorderHeight * 2;
			}
			else
			{
				//covers entire monitor
				shell.Left = location.X;
				shell.Top = location.Y;
				shell.Width = location.Width;
				shell.Height = location.Height;
			}
		}

		private void ShowMonitorWindow(Window window)
		{
			window.ShowActivated = true;
			window.Show();

			var monitorStartedEvent = this.eventAggregator.GetEvent<MonitorWindowShownEvent>();

			monitorStartedEvent.Publish(window);
		}

		private void LoadAddIn(ScrumSprintMonitorAddIn addIn)
		{
			this.logger.Info("Loading add-in {0}...", addIn.GetType().Name);

			addIn.Load();

			this.logger.Info("Loaded add-in {0}.", addIn.GetType().Name);
		}

		private void UnloadAddIn(ScrumSprintMonitorAddIn addIn)
		{
			this.logger.Info("Unloading add-in {0}...", addIn.GetType().Name);

			var disposable = addIn as IDisposable;
			addIn.Unload();
			if (disposable != null)
			{
				disposable.Dispose();
			}

			this.logger.Info("Unloaded add-in {0}.", addIn.GetType().Name);
		}

		private void UnloadAddIns()
		{
			if (AddIns != null)
			{
				foreach (var addIn in AddIns)
				{
					UnloadAddIn(addIn);
				}
				AddIns.Clear();
			}
		}

		private void OnApplicationExit(object sender, ExitEventArgs e)
		{
			this.eventAggregator.GetEvent<InvokeConfigurationUiEvent>().Unsubscribe(OnInvokeConfigurationUI);

			if (this.primaryMonitorShell != null)
			{
				this.logger.Info("Shutting down screensaver...");
			}

			if (this.powerManager != null)
			{
				this.powerManager.Dispose();
				this.powerManager = null;
			}

			UnloadAddIns();

			if (this.container != null)
			{
				this.container.Dispose();
			}

			//this.unityContainerAbstraction.Dispose();
		}

		private void OnInvokeConfigurationUI(Window obj)
		{
			ConfigureScreensaver();
			Settings.Default.Refresh();
		}

		private void OnApplicationDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
		{
			if (this.logger != null)
			{
				this.logger.Fatal(e.Exception, "Unhandled exception from {0}", sender);
			}
		}

		#region Window Message handling

		// ReSharper disable InconsistentNaming
		private const int WM_SYSCOMMAND = 0x112;
		private const int SC_MONITORPOWER = 0xF170;
		// ReSharper restore InconsistentNaming

		private IntPtr MessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
		{
			if (msg == WM_SYSCOMMAND && (((long) wParam & 0xFFF0) == SC_MONITORPOWER))
			{
				this.logger.Info("Received SC_MONITORPOWER message. Shutting down screensaver");
				ShutdownApplication();

				handled = true;
			}

			return IntPtr.Zero;
		}

		#endregion

		#endregion

		#region Overrides of UnityBootstrapper

		protected override IUnityContainer CreateContainer()
		{
			var unityContainer = base.CreateContainer();

			this.unityContainerAbstraction = UnityContainerAbstraction.EnsureContainerIsSetUp(unityContainer);

			return unityContainer;
		}

		[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
		protected override void ConfigureContainer()
		{
			base.ConfigureContainer();

			Container.RegisterType<PrimaryMonitorShell>();
			Container.RegisterType<SecondaryMonitorShell>();

			UnityContainerAbstraction.RegisterModule<UnityAppRuntimeModule>();
			UnityContainerAbstraction.RegisterModule<UnityAppConfigurationDependentModule>(true);
			this.unityContainerAbstraction.LoadRegisteredModules(false);

			var versionInfo = FileVersionInfo.GetVersionInfo(this.assembly.Location);

			this.eventAggregator = Container.Resolve<IEventAggregator>();
			this.logger = Container.Resolve<ILogger<App>>();
			this.logger.Info("Application version {0}.", versionInfo.FileVersion);

			if (Compose())
			{
				Container.Load(AdditionalContainerModules);

				foreach (var addIn in AddIns)
				{
					LoadAddIn(addIn);
				}
			}
			else
			{
				ShutdownApplication();
			}
		}

		[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
		protected override IModuleCatalog GetModuleCatalog()
		{
			return new DirectoryModuleCatalog
					{
						ModulePath = Path.GetDirectoryName(this.assembly.Location)
					};
		}

		[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
		protected override DependencyObject CreateShell()
		{
			if (this.isShuttingDown)
			{
				return null;
			}

			ProcessCommandLine();

			return this.primaryMonitorShell;
		}

		protected override void InitializeModules()
		{
			if (this.primaryMonitorShell == null)
			{
				return;
			}

			base.InitializeModules();

			var monitorStartedEvent = this.eventAggregator.GetEvent<MonitorStartedEvent>();

			monitorStartedEvent.Publish(EventArgs.Empty);
		}

		#endregion
	}
}